﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using System.Xml.Serialization;

namespace UmdhTracer
{

    public partial class UmdhTracer : Form
    {
        private bool _traceStarted;
        private string _selectedTraceId;
        private SerializableDictionary<string, BackTraces> _backtraces;
        private readonly ReaderWriterLockSlim _backtracesToken = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        public Settings UmdhTracerSettings;

        static public string AssemblyDirectory
        {
            get
            {
                string codeBase = Assembly.GetExecutingAssembly().CodeBase;
                UriBuilder uri = new UriBuilder(codeBase);
                string path = Uri.UnescapeDataString(uri.Path);
                return Path.GetDirectoryName(path);
            }
        }

        public UmdhTracer()
        {
            Settings.Load(out UmdhTracerSettings);
            

            _backtracesToken = new ReaderWriterLockSlim();
            _traceStarted = false;
            _backtraces = new SerializableDictionary<string, BackTraces>();
            
            InitializeComponent();
        }

        private void ApplySettings()
        {
            tbProcess.Text = UmdhTracerSettings.ExecutableName;
            tbPeriod.Text = UmdhTracerSettings.Period.ToString();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            infoToShow.SelectedIndex = 0;
            if (chartControl.Series != null)
            {
                chartControl.Series.Clear();    
            }
            ApplySettings();
            UpdateUi();
        }

        private void UpdateUi()
        {
            if (InvokeRequired)
            {
                BeginInvoke(new Action(UpdateUi));
                return;
            }

            startButton.Text = _traceStarted ? "Stop Tracing" : "Start Tracing";
            tbPeriod.Enabled = !_traceStarted;
            tbPeriodLabel.Enabled = !_traceStarted;
            importResultsButton.Enabled = !_traceStarted;
            saveResultsButton.Enabled = !_traceStarted;
            settingsButton.Enabled = !_traceStarted;
            tbProcess.Enabled = !_traceStarted;
            tbProcessLabel.Enabled = !_traceStarted;
        }

        private void TracesWorkerDoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;

            if (e.Argument is Settings == false || e.Argument as Settings == null)
            {
                UpdateStatus("Can't start dumps, settings are null");
                return;
            }

            Settings currentSettings = e.Argument as Settings;
            string umdhDirectory = Path.GetDirectoryName(currentSettings.UmdhPath);
            string umdhFilename = Path.GetFileName(currentSettings.UmdhPath);

            //BackgroundWorker worker = sender as BackgroundWorker;

            // Do First Dump
            UpdateStatus("First Dump");
            SpawnProcessWithWait(umdhDirectory, umdhFilename, "-pn:" + currentSettings.ExecutableName + " -f:\"" + AssemblyDirectory + "\\da.txt\"", currentSettings.UmdhTimeout * 60000);
            
            // Check cancellation pending
            if (CancellationInProgress(worker, e))
                return;


            for (int step = 1; step <= currentSettings.DumpCount; step++)
            {
                UpdateStatus("Dumping "+step+" trace");
                // Do Second Dump
                SpawnProcessWithWait(umdhDirectory, umdhFilename, "-pn:" + currentSettings.ExecutableName + " -f:\"" + AssemblyDirectory + "\\db.txt\"", currentSettings.UmdhTimeout * 60000);
                
                // Check cancellation pending
                if (CancellationInProgress(worker, e))
                    return;

                UpdateStatus("Comparing dumps " + step);
                // Compare Dump
                SpawnProcessWithWait(umdhDirectory, umdhFilename, "\"" + AssemblyDirectory + "\\da.txt\" \"" + AssemblyDirectory + "\\db.txt\" -f:\"" + AssemblyDirectory + "\\Result" + step + ".txt\"", currentSettings.UmdhTimeout * 60000);

                // Check cancellation pending
                if (CancellationInProgress(worker, e))
                    return;
                
                // Extract Info
                ParseResult("" + AssemblyDirectory + "\\Result" + step + ".txt");

                UpdateStatus("Updating BackTrace " + step);
                UpdateBacktraces();

                // Check cancellation pending
                if (CancellationInProgress(worker, e))
                    return;

                int timeToWait = currentSettings.Period * 60;
                do
                {
                    if (CancellationInProgress(worker, e))
                        return;
                    
                    timeToWait--;
                    Thread.Sleep(1000);
                    UpdateStatus("Next dump in " + timeToWait/60 +":"+timeToWait%60+"s");

                } while (timeToWait > 0);
            }

            return;
        }

        public void UpdateBacktraces()
        {
            if (InvokeRequired)
            {
                BeginInvoke(new Action(UpdateBacktraces));
                return;
            }

            lvBackTraces.Items.Clear();
            lvBackTraces.BeginUpdate();
            chartControl.BeginInit();

            List<BackTraces> backTracesToAdd = null;
            _backtracesToken.EnterReadLock();
            try
            {
                backTracesToAdd = _backtraces.Values.ToList();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Catch occur: " + ex.Message);
                Debug.Assert(false);
            }
            _backtracesToken.ExitReadLock();

            try
            {
                foreach (BackTraces backTrace in backTracesToAdd)
                {
                    BackTraceMeasurement lastMeasurement = backTrace.Measurements.Values.Last();
                    BackTraceMeasurement firstMeasurement = backTrace.Measurements.Values.First();

                    if (lastMeasurement.AllocDelta - firstMeasurement.AllocDelta > 0)
                    {
                        ListViewItem lv = new ListViewItem(backTrace.Id);

                        int deltaAllocFromFirst = lastMeasurement.AllocDelta - firstMeasurement.AllocDelta;
                        int meanAllocFromFirst = 0;
                        int deltaBytesFromFirst = lastMeasurement.BytesDelta - firstMeasurement.BytesDelta;
                        int meanBytesFromFirst = 0;

                        foreach (BackTraceMeasurement backTraceMeasurement in backTrace.Measurements.Values)
                        {
                            meanAllocFromFirst += backTraceMeasurement.AllocDelta;
                            meanBytesFromFirst += backTraceMeasurement.BytesDelta;
                        }

                        meanAllocFromFirst /= backTrace.Measurements.Values.Count;
                        meanBytesFromFirst /= backTrace.Measurements.Values.Count;

                        lv.SubItems.Add((deltaAllocFromFirst).ToString());
                        lv.SubItems.Add((meanAllocFromFirst).ToString());


                        lv.SubItems.Add((deltaBytesFromFirst).ToString());
                        lv.SubItems.Add((meanBytesFromFirst).ToString());

                        // If there was no free on this backtrace, display it in red
                        if (lastMeasurement.AllocNew > 0 && lastMeasurement.AllocOld == 0)
                        {
                            lv.ForeColor = Color.Red;
                        }

                        lvBackTraces.Items.Add(lv);

                        AddBackTraceSerie(backTrace.Id);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Catch occur: " + ex.Message);
                Debug.Assert(false);
            }

            chartControl.EndInit();
            lvBackTraces.EndUpdate();
        }

        public bool CancellationInProgress(BackgroundWorker worker, DoWorkEventArgs e)
        {
            if (worker.CancellationPending)
            {
                UpdateStatus("Worker Cancelled");
                e.Cancel = true;
                return true;
            }
            return false;
        }

        public static bool SpawnProcessWithWait(string workingDirectory, string processFilename, string arguments, int timeoutInMs)
        {
            bool bRetVal = true;
            try
            {
                Debug.WriteLine("SpawnProcessWithWait: " + workingDirectory + "\\" + processFilename + " " + arguments);
                ProcessStartInfo startInfo = new ProcessStartInfo();
                startInfo.FileName = workingDirectory + "\\" + processFilename;
                startInfo.UseShellExecute = false;
                startInfo.CreateNoWindow = true;
                startInfo.WindowStyle = ProcessWindowStyle.Hidden;

                startInfo.WorkingDirectory = workingDirectory;
                startInfo.Arguments = arguments;
                Process p = Process.Start(startInfo);

                Debug.WriteLine("SpawnProcessWithWait: '" + processFilename + "'");
                p.WaitForExit(timeoutInMs); //give time to the server to sign, but protect against lock
                if (p.HasExited == false)
                {
                    throw new Exception("The process didn't respond in a timely fashion");
                }

                Debug.WriteLine(" - ExitCode: " + p.ExitCode);
                    
                if( p.ExitCode != 0 )
                {
                    bRetVal = false;
                }
                p.Close();

            }
            catch (Exception ex)
            {
                Debug.WriteLine(false, "Can't spawn process: " + workingDirectory + "\\" + processFilename + " - " + arguments + " Ex: " + ex.ToString());
                bRetVal = false;
            }
            return bRetVal;
        }



        private void ParseResult(string resultPath)
        {
            StreamReader re = null;

            UpdateStatus("Parsing result: " + resultPath);

            try
            {
                DateTime parseResultDate = DateTime.Now;
                re = File.OpenText(resultPath);
                string input = null;

                bool parseStarted = false;

                BackTraceMeasurement backTraceMeasurement = new BackTraceMeasurement();

                string id = "";
                string description = "";
                
                while ((input = re.ReadLine()) != null)
                {
                    // Start parsing when the first trace is found
                    if (!parseStarted && (input.StartsWith("+") || input.StartsWith("-")))
                    {
                        parseStarted = true;
                    }

                    if (parseStarted)
                    {
                        if (input.StartsWith("+") || input.StartsWith("-"))
                        {
                            //                                                                          
                            // Each log entry has the following syntax:                                 
                            //                                                                          
                            // + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID 
                            // + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations      
                            //     ... stack trace ...     
                        
                            bool bytesDeltaToSet;
                            if (input.Contains("allocs"))
                            {
                                // Reading bytesDelta & BackTrace Id
                                bytesDeltaToSet = true;

                                // If Id has been set, add the previous backtrace
                                if (!string.IsNullOrWhiteSpace(id))
                                {
                                    _backtracesToken.EnterWriteLock();
                                    try
                                    {
                                        if (!_backtraces.ContainsKey(id))
                                        {
                                            _backtraces[id] = new BackTraces(id, description);
                                        }
                                        _backtraces[id].AddMeasure(parseResultDate, backTraceMeasurement);
                                        backTraceMeasurement = new BackTraceMeasurement();
                                    }
                                    catch (Exception ex)
                                    {
                                        Debug.WriteLine("Catch occur: " + ex.Message);
                                        Debug.Assert(false);
                                    }
                                    _backtracesToken.ExitWriteLock();
                                }

                            }
                            else if (input.Contains("allocations"))
                            {
                                // Reading allocDelta
                                bytesDeltaToSet = false;
                                description = "";
                            }
                            else
                            {
                                Debug.Assert(false);
                                return;
                            }

                            input = input.Replace("(", " ");
                            input = input.Replace("+", "+ ");
                            input = input.Replace(")", " ");
                            input = input.Replace("\t", " ");
                            input = input.Replace("allocs", " ");
                            input = input.Replace("allocations", " ");
                            input = input.Replace("BackTrace", " ");
                            string[] inputSplit = input.Split(new char[] { ' ' }, 10, StringSplitOptions.RemoveEmptyEntries);

                            // After split we have:
                            //                                                                          
                            // Each log entry has the following syntax:                                 
                            //                                                                          
                            // + BYTES_DELTA NEW_BYTES - OLD_BYTES NEW_COUNT TRACEID 
                            // + COUNT_DELTA NEW_COUNT - OLD_COUNT TRACEID      
                            //     ... stack trace ...     

                            const int DeltaSign = 0;
                            const int DeltaAbsoluteValue = 1;
                            const int NewValue = 2;
                            const int OldValue = 4;
                            const int TraceIdOrNull = 6;

                            if ((inputSplit.Count() == 7 && bytesDeltaToSet) || (inputSplit.Count() == 6 && !bytesDeltaToSet))
                            {
                                int delta = Convert.ToInt32(inputSplit[DeltaAbsoluteValue], 16);
                                int newValue = Convert.ToInt32(inputSplit[NewValue], 16);
                                int oldValue = Convert.ToInt32(inputSplit[OldValue], 16);

                                if (inputSplit[DeltaSign] == "-")
                                {
                                    delta *= -1;
                                }

                                if (bytesDeltaToSet)
                                {
                                    id = inputSplit[TraceIdOrNull];
                                    backTraceMeasurement.BytesDelta = delta;
                                    backTraceMeasurement.BytesNew = newValue;
                                    backTraceMeasurement.BytesOld = oldValue;
                                }
                                else
                                {
                                    backTraceMeasurement.AllocDelta = delta;
                                    backTraceMeasurement.AllocNew = newValue;
                                    backTraceMeasurement.AllocOld = oldValue;
                                }
                            }
                            else
                            {
                                Debug.Assert(false);
                            }
                        }
                        else
                        {
                            if (!string.IsNullOrWhiteSpace(input))
                            {
                                description += input.TrimStart(new[] { ' ', '\t' });
                                description += "\r\n";
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("ParseResult Catch Occur: "+ex.Message);
            }

            if (re != null)
            {
                re.Close();
            }
        }

        private void UpdateStatus(string message)
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new Action<string>(UpdateStatus), message);
                return;
            }

            statusLabel.Text = message;
        }

        private void tracesWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {

        }

        private void TracesWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            _traceStarted = false;

            UpdateStatus("Worker Completed");

            UpdateUi();
        }

        private void LvBackTracesSelectedIndexChanged(object sender, EventArgs e)
        {
            if (lvBackTraces.SelectedItems.Count > 0)
            {
                _selectedTraceId = lvBackTraces.SelectedItems[0].Text;
                //chartControl.Series.Clear();
                //AddBackTraceSerie(_selectedTraceId);
                _backtracesToken.EnterReadLock();
                try
                {
                    BackTraces backTraces = _backtraces[_selectedTraceId];
                    string newLineFromXML = "";
                    if (backTraces.Description.Contains("\r\n"))
                    {
                        newLineFromXML = "\r\n";
                    }
                    else if (backTraces.Description.Contains("\r"))
                    {
                        newLineFromXML = "\r";
                    } 
                    else if (backTraces.Description.Contains("\n"))
                    {
                        newLineFromXML = "\n";
                    }
                    backTraceDetail.Text = backTraces.Description.Replace(newLineFromXML, "\r\n");
                    foreach (Series series in chartControl.Series)
                    {
                        if (series.Tag != null && series.Tag.ToString() == _selectedTraceId)
                        {
                            series.BorderWidth = 3;
                        }
                        else
                        {
                            series.BorderWidth = 1;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("Catch occur: " + ex.Message);
                    Debug.Assert(false);
                }
                _backtracesToken.ExitReadLock();
            }
        }
        
        private void AddBackTraceSerie(string id)
        {
            if (chartControl.InvokeRequired)
            {
                chartControl.BeginInvoke(new Action<string>(AddBackTraceSerie), id);
                return;
            }

            _backtracesToken.EnterReadLock();
            try
            {
                BackTraces backTraces = _backtraces[id];

                Series seriesToDelete = null;
                for (int i = 0; i < chartControl.Series.Count; i++)
                {
                    if (chartControl.Series[i].Tag != null && chartControl.Series[i].Tag.ToString() == id)
                    {
                        seriesToDelete = chartControl.Series[i];
                        break;
                    }
                }

                if (seriesToDelete != null)
                {
                    chartControl.Series.Remove(seriesToDelete);
                }

                Series serie = new Series { LegendText = backTraces.Id, ChartType = SeriesChartType.Line, Tag = backTraces.Id };

                var items = from delta in backTraces.Measurements
                            orderby delta.Key ascending
                            select delta.Value;
                var firstItem = items.First();

                // Add Backtrace only if average is > 0 && last value > 0
                int step = 1;
                for (int i = 0; i < items.Count(); i++)
                {
                    int value = 0;
                    if (i > 0)
                    {
                        if (infoToShow.SelectedIndex == 0)
                        {
                            value = items.ElementAt(i).AllocDelta - firstItem.AllocDelta;
                        }
                        else
                        {
                            value = items.ElementAt(i).BytesDelta - firstItem.BytesDelta;
                        }                        
                    }
                    serie.Points.Add(new DataPoint(i+1, value));
                }
                chartControl.Series.Add(serie);
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Catch occur: " + ex.Message);
                Debug.Assert(false);
            }
            _backtracesToken.ExitReadLock();
        }    

        private void LvBackTracesColumnClick(object sender, ColumnClickEventArgs e)
        {
            SortList(e.Column);
        }

        private void SortList(int colIndex, bool forceDown = false)
        {
            if (this.InvokeRequired)
            {
                BeginInvoke(new Action<int, bool>(SortList), colIndex, forceDown);
                return;
            }

            //
            // Set the ListViewItemSorter property to a new ListViewItemComparer 
            // object. Setting this property immediately sorts the 
            // ListView using the ListViewItemComparer object.
            bool sortAsc;
            if (forceDown)
            {
                sortAsc = false;
                lvBackTraces.Columns[colIndex].Tag = null;
            }
            else
            {
                if (lvBackTraces.Columns[colIndex].Tag == null)
                {
                    sortAsc = true;
                    lvBackTraces.Columns[colIndex].Tag = true;
                }
                else
                {
                    sortAsc = false;
                    lvBackTraces.Columns[colIndex].Tag = null;
                }
            }
            this.lvBackTraces.ListViewItemSorter = new ListViewItemComparer(colIndex, sortAsc);

        }

        private void dumpPeriodTextBox_KeyPress(object sender, KeyPressEventArgs e)
        {
            if( (e.KeyChar < '0' || e.KeyChar > '9') && e.KeyChar != '\b')
            {
                e.Handled = true;
            }
        }

        private void quit_Click(object sender, EventArgs e)
        {
            if (MessageBox.Show("Are you sure you want to close UMDH Tracer ?", "Question", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
            {
                this.Close();   
            }
        }

        private void StartButtonClick(object sender, EventArgs e)
        {
            _traceStarted = !_traceStarted;
            if (_traceStarted)
            {
                UmdhTracerSettings.Period = Convert.ToInt16(tbPeriod.Text);
                UmdhTracerSettings.ExecutableName = tbProcess.Text;

                if(UmdhTracerSettings.Period <= 0)
                {
                    MessageBox.Show("Dump Period can't be less than on minute");
                    _traceStarted = !_traceStarted;
                    return;
                }

                if (string.IsNullOrWhiteSpace(UmdhTracerSettings.ExecutableName))
                {
                    MessageBox.Show("ExecutableName can't be empty");
                    _traceStarted = !_traceStarted;
                    return;
                }

                chartControl.Series.Clear();
                lvBackTraces.Items.Clear();

                tracesWorker.RunWorkerAsync(UmdhTracerSettings.Clone());
            }
            else
            {
                tracesWorker.CancelAsync();
            }
            UpdateUi();
        }

        private void SaveResultsButtonClick(object sender, EventArgs e)
        {
            try
            {
                saveBacktraceDialog.InitialDirectory = AssemblyDirectory;
                if( saveBacktraceDialog.ShowDialog() == DialogResult.OK)
                {
                    XmlSerializer serializerObj = new XmlSerializer(typeof(SerializableDictionary<string, BackTraces>));
                    // Create a new file stream to write the serialized object to a file
                    TextWriter writeFileStream = new StreamWriter(saveBacktraceDialog.FileName);
                    serializerObj.Serialize(writeFileStream, _backtraces);
                    // Cleanup
                    writeFileStream.Close();
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show("Failed to save Backtraces : " + ex.Message);
            }

        }

        private void ImportResultsButtonClick(object sender, EventArgs e)
        {
            openBacktraceDialog.InitialDirectory = AssemblyDirectory;
            if (openBacktraceDialog.ShowDialog() == DialogResult.OK)
            {
                XmlSerializer serializerObj = new XmlSerializer(typeof(SerializableDictionary<string, BackTraces>));
                FileStream readFileStream = new FileStream(openBacktraceDialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                _backtraces = (SerializableDictionary<string, BackTraces>)serializerObj.Deserialize(readFileStream);
                readFileStream.Close();
                UpdateBacktraces();
            }
        }

        private void SettingsClick(object sender, EventArgs e)
        {
            UmdhTracerSettings settingsForm = new UmdhTracerSettings(UmdhTracerSettings);

            if(settingsForm.ShowDialog() == DialogResult.OK)
            {
                UmdhTracerSettings = settingsForm.SettingsEdited.Clone();
                ApplySettings();
            }
        }

        private void UmdhTracerFormClosed(object sender, FormClosedEventArgs e)
        {
            UmdhTracerSettings.Save();
        }

        private void AboutClick(object sender, EventArgs e)
        {
            MessageBox.Show("UMDH Tracer V1.0\r\n\r\nhttp://umdhtracer.codeplex.com");
        }


    }

    // Implements the manual sorting of items by columns.
    class ListViewItemComparer : IComparer
    {
        private readonly int _col;
        private readonly bool _sortAsc;
        public ListViewItemComparer()
        {
            _col = 0;
            _sortAsc = true;
        }

        public ListViewItemComparer(int column, bool sortAsc)
        {
            _sortAsc = sortAsc;
            _col = column;
        }

        public int Compare(object x, object y)
        {
            switch (_col)
            {
                case 1:
                case 2:
                case 3:
                case 4:
                    return (Convert.ToInt32(((ListViewItem)x).SubItems[_col].Text) - Convert.ToInt32(((ListViewItem)y).SubItems[_col].Text)) * (_sortAsc ? 1 : -1);
                default:
                    return String.Compare(((ListViewItem)x).SubItems[_col].Text, ((ListViewItem)y).SubItems[_col].Text) * (_sortAsc ? 1 : -1);

            }
        }
    }
}
